package jxtend.processor.eclipse.visitor.handler.impl;

import jxtend.annotation.keyword.reassigned;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.AbstractASTNodeVisitorHandler;
import jxtend.processor.eclipse.visitor.handler.ProblemSeverity;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ForeachStatement;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TryStatement;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;

@SuppressWarnings("restriction")
public class ImplicitFinalLocalDeclarationHandler extends AbstractASTNodeVisitorHandler {

	private static final char[][] compoundNameOfReassignedAnnotation =
		EclipseProcessorUtils.compoundNameOfClassName(reassigned.class.getName());

	@Override
	public boolean endVisitBeforeResolve(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		if (!(scope instanceof BlockScope) || !(node instanceof TypeReference)) {
			return true;
		}

		ASTNode parentNode = context.getParentNode();

		if (!(parentNode instanceof LocalDeclaration)
				|| (node != ((LocalDeclaration) parentNode).type)
				|| (!((parentNode instanceof Argument)
						&& ((context.getParentNode(1) instanceof AbstractMethodDeclaration) 
								|| (context.getParentNode(1) instanceof TryStatement)))
					&& !(context.getParentNode(1) instanceof ForeachStatement))) {
			return true;
		}

		LocalDeclaration localDeclaration = (LocalDeclaration) parentNode;

		Annotation reassignedAnnotation = getReassignedAnnotation(localDeclaration, (BlockScope) scope);
		boolean existingFinalModifier = ((localDeclaration.modifiers & ClassFileConstants.AccFinal) != 0);
		
		if (reassignedAnnotation != null) {
			if (existingFinalModifier) {
				context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
						reassignedAnnotation.sourceStart, reassignedAnnotation.sourceEnd,
						"Cannot combine a @reassigned with explicit final modifier");
			}
		} else if (!TypeUtils.isValOrVarType(localDeclaration.type)) {
			if (existingFinalModifier) {
				context.recordProblemForNode(ProblemSeverity.WARNING, context.getCurrentReferenceContext(),
						localDeclaration.declarationSourceStart, localDeclaration.declarationSourceEnd,
						"Unneeded explicit final modifier");
			} else {
				localDeclaration.modifiers |= ClassFileConstants.AccFinal;

				if (localDeclaration.binding != null) {
					localDeclaration.binding.modifiers |= ClassFileConstants.AccFinal;
				}
			}
		}

		return true;
	}

	private Annotation getReassignedAnnotation(LocalDeclaration localDeclaration, BlockScope blockScope) {
		if (localDeclaration.annotations == null) { return null; }

		for (Annotation annotation : localDeclaration.annotations) {
			TypeBinding annotationTypeBinding;

			if (annotation.type instanceof QualifiedTypeReference) {
				char[][] compoundName = ((QualifiedTypeReference) annotation.type).tokens;

				annotationTypeBinding = blockScope.getType(compoundName, compoundName.length);
			} else {
				char[][] jxtendKeywordAnnotationCompoundName = new char[][] {
						JxtendTypeConstants.JXTEND, JxtendTypeConstants.ANNOTATION, JxtendTypeConstants.KEYWORD,
						((SingleTypeReference) annotation.type).token };

				annotationTypeBinding = blockScope.getType(
						jxtendKeywordAnnotationCompoundName, jxtendKeywordAnnotationCompoundName.length);
			}

			if (EclipseProcessorUtils.isValidTypeReference(annotationTypeBinding)
				&& CharOperation.equals(
						compoundNameOfReassignedAnnotation, ((ReferenceBinding) annotationTypeBinding).compoundName)) {
				return annotation;
			}
		}

		return null;
	}
}
